package com.example.jpa.crud.service;

import com.example.jpa.crud.exception.ParameterTypeConvertException;
import com.example.jpa.crud.exception.RequestBodyMissingException;
import com.example.jpa.crud.search.PageRequest;
import com.example.jpa.crud.search.PageResponse;
import com.example.jpa.crud.search.SearchCondition;
import com.example.jpa.crud.search.SortCondition;
import com.example.jpa.crud.repository.SimpleCrudRepository;
import com.example.jpa.crud.model.ResourceModel;
import com.example.jpa.crud.model.ResourceModelSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;

import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Optional;
import java.util.Arrays;

@Service
public class SimpleCrudService {

    private static final Logger log = LoggerFactory.getLogger(SimpleCrudService.class);

    private final ResourceModelSupport resourceModelSupport;

    private final SimpleCrudRepository simpleCrudRepository;

    private final JsonService jsonService;

    public SimpleCrudService(
            ResourceModelSupport resourceModelSupport,
            SimpleCrudRepository simpleCrudRepository,
            JsonService jsonService
    ) {
        this.resourceModelSupport = resourceModelSupport;
        this.simpleCrudRepository = simpleCrudRepository;
        this.jsonService = jsonService;
    }

    private Object parseId(Class<?> idClass, String rawId) {
        if (idClass == null) {
            return rawId;
        }
        if (Long.class.equals(idClass) || long.class.equals(idClass)) {
            return Long.valueOf(rawId);
        }
        if (Integer.class.equals(idClass) || int.class.equals(idClass)) {
            return Integer.valueOf(rawId);
        }
        if (String.class.equals(idClass)) {
            return rawId;
        }
        return idClass.cast(rawId);
    }

    public Object create(String name, byte[] data) {
        final ResourceModel model = resourceModelSupport.getResourceModelByName(name);
        final Optional<Map<String, Object>> optional = jsonService.fromBytes(data);
        if (!optional.isPresent()) {
            throw new RequestBodyMissingException();
        }
        final Map<String, Object> entity = optional.get();
        final Map<String, Object> object = simpleCrudRepository.create(model, entity);
        return resourceModelSupport.getResponseTemplate().ok(object, true, object == null ? 0L : 1L);
    }

    public Object findById(String name, String rawId) {
        final ResourceModel model = resourceModelSupport.getResourceModelByName(name);
        Object object = null;
        try {
            Object id = parseId(model.getIdClass(), rawId);
            object = simpleCrudRepository.findById(model, id);
        } catch (Exception e) {
            log.error("findById", e);
        }
        return resourceModelSupport.getResponseTemplate().ok(object, false, 0L);
    }

    public Object updateById(String name, String rawId, byte[] data) {
        final ResourceModel model = resourceModelSupport.getResourceModelByName(name);
        final Optional<Map<String, Object>> optional = jsonService.fromBytes(data);
        if (!optional.isPresent()) {
            throw new RequestBodyMissingException();
        }
        final Map<String, Object> entity = optional.get();
        final Object id = parseId(model.getIdClass(), rawId);
        final int effect = simpleCrudRepository.updateById(model, id, entity);
        return resourceModelSupport.getResponseTemplate().ok(null, true, effect);
    }

    public Object deleteById(String name, String rawId) {
        final ResourceModel model = resourceModelSupport.getResourceModelByName(name);
        final Object id = parseId(model.getIdClass(), rawId);
        final int effect = simpleCrudRepository.deleteById(model, id);
        return resourceModelSupport.getResponseTemplate().ok(null, true, effect);
    }

    public Object search(String name, Map<String, String[]> parameterMap) {
        final PageRequest pageRequest = buildPageRequest(parameterMap);
        final PageResponse pageResponse = search(name, pageRequest);
        return resourceModelSupport.getResponseTemplate().ok(pageResponse, false, 0L);
    }

    public Object search(String name, MultiValueMap<String, String> parameterMap) {
        final PageRequest pageRequest = buildPageRequest(parameterMap);
        final PageResponse pageResponse = search(name, pageRequest);
        return resourceModelSupport.getResponseTemplate().ok(pageResponse, false, 0L);
    }

    private PageResponse search(String name, PageRequest pageRequest) {
        final ResourceModel model = resourceModelSupport.getResourceModelByName(name);
        Long page = pageRequest.getPage();
        Long size = pageRequest.getSize();
        if ((page != null && page <= 0L) || (size != null && size <= 0L)) {
            final PageResponse pageResponse = PageResponse.empty();
            pageResponse.setPage(page);
            pageResponse.setSize(size);
            return pageResponse;
        }
        final String search = pageRequest.getSearch();
        final Map<String, List<String>> params = pageRequest.getParams();
        final SearchCondition searchCondition = simpleCrudRepository.resolveSearchExpression(model, search, params);
        final Long total = simpleCrudRepository.count(model, searchCondition);
        if (total == null || total == 0L) {
            final PageResponse pageResponse = PageResponse.empty();
            pageResponse.setPage(page);
            pageResponse.setSize(size);
            return pageResponse;
        }
        final String sort = pageRequest.getSort();
        final SortCondition sortCondition = simpleCrudRepository.resolveSortExpression(model, sort);
        if (page == null) {
            page = 1L;
        }
        if (size == null) {
            size = 10L;
        }
        final List<Map<String, Object>> list = simpleCrudRepository.search(model, searchCondition, sortCondition, page, size);
        final PageResponse pageResponse = new PageResponse();
        pageResponse.setPage(page);
        pageResponse.setSize(size);
        pageResponse.setData(list);
        pageResponse.setTotal(total);
        pageResponse.setTotalPage(total / size + 1L);
        return pageResponse;
    }

    private static PageRequest buildPageRequest(Map<String, String[]> parameterMap) {
        final PageRequest pageRequest = new PageRequest();
        final Map<String, List<String>> params = new HashMap<>();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            final String key = entry.getKey();
            final String[] value = entry.getValue();
            if (key == null || value == null) {
                continue;
            }
            switch (key) {
                case "page":
                    if (value.length > 0) {
                        final String s = value[value.length - 1];
                        final long page = parseLongValue(s);
                        pageRequest.setPage(page);
                    }
                    break;
                case "size":
                    if (value.length > 0) {
                        final String s = value[value.length - 1];
                        final long size = parseLongValue(s);
                        pageRequest.setSize(size);
                    }
                    break;
                case "sort":
                    if (value.length > 0) {
                        pageRequest.setSort(value[value.length - 1]);
                    }
                    break;
                case "search":
                    if (value.length > 0) {
                        pageRequest.setSearch(value[value.length - 1]);
                    }
                    break;
                default:
                    params.put(key, new ArrayList<>(Arrays.asList(value)));
                    break;
            }
        }
        pageRequest.setParams(params);
        return pageRequest;
    }

    private static PageRequest buildPageRequest(MultiValueMap<String, String> parameterMap) {
        final PageRequest pageRequest = new PageRequest();
        final Map<String, List<String>> params = new HashMap<>();
        for (Map.Entry<String, List<String>> entry : parameterMap.entrySet()) {
            final String key = entry.getKey();
            final List<String> value = entry.getValue();
            if (key == null || value == null) {
                continue;
            }
            switch (key) {
                case "page":
                    if (!value.isEmpty()) {
                        final String s = value.get(value.size() - 1);
                        final long page = parseLongValue(s);
                        pageRequest.setPage(page);
                    }
                    break;
                case "size":
                    if (!value.isEmpty()) {
                        final String s = value.get(value.size() - 1);
                        final long size = parseLongValue(s);
                        pageRequest.setSize(size);
                    }
                    break;
                case "sort":
                    if (!value.isEmpty()) {
                        pageRequest.setSort(value.get(value.size() - 1));
                    }
                    break;
                case "search":
                    if (!value.isEmpty()) {
                        pageRequest.setSearch(value.get(value.size() - 1));
                    }
                    break;
                default:
                    params.put(key, new ArrayList<>(value));
                    break;
            }
        }
        pageRequest.setParams(params);
        return pageRequest;
    }

    private static long parseLongValue(String s) {
        long value;
        try {
            value = Long.parseLong(s);
        } catch (NumberFormatException e) {
            throw new ParameterTypeConvertException("'" + s + "' is not a valid integer64 value");
        }
        return value;
    }

}
